package com.gitee.wsl.compose.chart.pie.label

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.Placeable
import com.gitee.wsl.compose.chart.frond.ExternalLabelPosition
import com.gitee.wsl.compose.chart.frond.InternalLabelPosition
import com.gitee.wsl.compose.chart.frond.LabelPosition
import com.gitee.wsl.compose.chart.frond.None
import com.gitee.wsl.compose.chart.pie.model.PieSliceData
import com.gitee.wsl.compose.chart.pie.model.SliceLabelData
import com.gitee.wsl.compose.chart.y2theta
import com.gitee.wsl.mathematics.coordinate.polar.Polar
import com.gitee.wsl.unit.angle.Quadrant
import com.gitee.wsl.unit.angle.isNorth
import com.gitee.wsl.unit.angle.isSouth
import com.gitee.wsl.unit.angle.isWest
import com.gitee.wsl.mathematics.coordinate.polar.ext.cartesianToPolar
import com.gitee.wsl.unit.angle.deg
import com.gitee.wsl.unit.angle.ext.div
import com.gitee.wsl.unit.angle.ext.plus
import com.gitee.wsl.mathematics.coordinate.polar.ext.polarToCartesian
import com.gitee.wsl.unit.angle.ext.cosF
import com.gitee.wsl.unit.angle.ext.sinF
import com.gitee.wsl.unit.angle.quadrant
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min

/**
 * Places labels circularly around the outer perimeter of the pie, adjusting them vertically to not overlap with
 * adjacent labels.
 *
 * @param labelSpacing A value greater than 1 specifying the distance from the center of
 * the pie at which to place external labels relative to the overall diameter of the pie, where a value
 * of 1 is at the outer edge of the pie. Values between 1.05 and 1.4 tend to work well depending
 * on the size of the labels and overall pie diameter.
 * @param labelPlacement Specifies the locations the labels may be placed, including within the slice or
 * externally. @see [PieLabelPlacement].
 */
 class CircularLabelPositionProvider(
    private val labelSpacing: Float,
    private val labelPlacement: PieLabelPlacement = PieLabelPlacement.External,
) : LabelPositionProvider {
    override fun computeLabelPositions(
        pieDiameter: Float,
        holeSize: Float,
        placeables: List<Placeable>,
        pieSliceData: List<PieSliceData>
    ): List<LabelPosition> {
        return computeLabelPositions(
            PieParameters(pieDiameter, holeSize, labelPlacement),
            groupLabels(placeables, pieSliceData)
        )
    }

    // Group slice labels by their quadrant, with related data
    private fun groupLabels(
        labelPlaceables: List<Placeable>,
        pieSliceDatas: List<PieSliceData>,
    ): Map<Quadrant, List<SliceLabelData>> {
        val sliceGroups = mutableMapOf<Quadrant, MutableList<SliceLabelData>>()

        labelPlaceables.forEachIndexed { index, placeable ->
            val centerAngle = pieSliceDatas[index].startAngle + (pieSliceDatas[index].angle / 2f)

//            val matchingQuadrant = Quadrant.entries.first {
//                it.angleRange.contains(centerAngle.degreesF)
//            }

            val matchingQuadrant = centerAngle.quadrant

            sliceGroups[matchingQuadrant] =
                (sliceGroups.getOrElse(matchingQuadrant) { ArrayList() }).apply {
                    add(SliceLabelData(index, pieSliceDatas[index], placeable, centerAngle))
                }
        }

        return sliceGroups
    }


    private fun computeInQuadrant(
        pieParameters: PieParameters,
        sliceLabelData: SliceLabelData,
        quadrant: Quadrant,
        yState: YState
    ): LabelPosition {
        var pos1: LabelPosition? = null
        if (pieParameters.labelPlacement !is PieLabelPlacement.External) {
            // Try internal placement first
            pos1 = computeInternalLabelOffset(pieParameters, sliceLabelData)
        }

        if (pos1 == null || (pos1 is None && pieParameters.labelPlacement is PieLabelPlacement.InternalOrExternal)) {
            // do external label placement
            pos1 = computeExternalLabelOffset(pieParameters, sliceLabelData, quadrant, yState.last)

            if (quadrant.isNorth()) {
                yState.last = pos1.position.y
                // update maxY
                yState.max = max(yState.max, pos1.position.y + sliceLabelData.labelPlaceable.height)
            } else {
                // update lastY
                yState.last = pos1.position.y + sliceLabelData.labelPlaceable.height
            }
        }

        return pos1
    }

    private fun computeLabelPositions(
        pieParameters: PieParameters,
        sliceGroups: Map<Quadrant, List<SliceLabelData>>,
    ): List<LabelPosition> {
        // create a mapping of slice index to label offset for sorting back to a list later
        val sliceOffsetMap = mutableMapOf<Int, LabelPosition>()

        // yState keeps track of the last external label's y-axis position so the next
        // external label can position itself without interfering
        val yState = YState(Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY)
        sliceGroups[Quadrant.NorthEast]?.reversed()?.forEach {
            sliceOffsetMap[it.index] = computeInQuadrant(pieParameters, it,
                Quadrant.NorthEast, yState)
        }

        yState.last = yState.max
        sliceGroups[Quadrant.SouthEast]?.forEach {
            sliceOffsetMap[it.index] = computeInQuadrant(pieParameters, it,
                Quadrant.SouthEast, yState)
        }

        yState.last = Float.POSITIVE_INFINITY
        yState.max = Float.NEGATIVE_INFINITY
        sliceGroups[Quadrant.NorthWest]?.forEach {
            sliceOffsetMap[it.index] = computeInQuadrant(pieParameters, it,
                Quadrant.NorthWest, yState)
        }

        yState.last = yState.max
        sliceGroups[Quadrant.SouthWest]?.reversed()?.forEach {
            sliceOffsetMap[it.index] = computeInQuadrant(pieParameters, it,
                Quadrant.SouthWest, yState)
        }

        return sliceOffsetMap.entries.sortedBy { it.key }.map { it.value }
    }

    /**
     * Computes the position and anchor (line connection) position for a label when external label placement
     * is used.
     * @param sliceLabelData slice label data
     * @param quadrant Quadrant in which the label is located
     * @param lastY min or max Y-coordinate of the previous label this label cannot interfere with
     */
    private fun computeExternalLabelOffset(
        pieParameters: PieParameters,
        sliceLabelData: SliceLabelData,
        quadrant: Quadrant,
        lastY: Float,
    ): ExternalLabelPosition {
        val angle = sliceLabelData.centerAngle
        val placeable = sliceLabelData.labelPlaceable
        val labelDiameter = pieParameters.pieDiameter * labelSpacing
        val isLeftSide = quadrant.isWest()
        val labelTop = if (quadrant.isSouth()) {
            max(labelDiameter / 2f * angle.sinF() - placeable.height / 2.0F, lastY)
        } else {
            min(
                labelDiameter / 2f * angle.sinF() - placeable.height / 2.0F,
                lastY - placeable.height
            )
        }
        val labelBottom = labelTop + placeable.height

        val thetas1 = y2theta(labelTop, labelDiameter / 2f)
        val thetas2 = y2theta(labelBottom, labelDiameter / 2f)

        // xLimit is the x-axis position closest to the center of the pie allowed
        val xLimit = (labelDiameter / 2f) * angle.cosF()

        // thetas1 or thetas2 could be NaN if the y-coordinate was larger than labelDiameter / 2
        val xOffset =
            if (isLeftSide) {
                val xMin1 = if (thetas1.first.isNaN()) {
                    Float.POSITIVE_INFINITY
                } else {
                    labelDiameter / 2f * min(cos(thetas1.first), cos(thetas1.second))
                }

                val xMin2 = if (thetas2.first.isNaN()) {
                    Float.POSITIVE_INFINITY
                } else {
                    labelDiameter / 2f * min(cos(thetas2.first), cos(thetas2.second))
                }

                if (xMin1.isInfinite() && xMin2.isInfinite()) {
                    xLimit
                } else {
                    min(min(xMin1, xMin2), xLimit)
                }
            } else {
                val xMax1 = if (thetas1.first.isNaN()) {
                    Float.NEGATIVE_INFINITY
                } else {
                    labelDiameter / 2f * max(cos(thetas1.first), cos(thetas1.second))
                }

                val xMax2 = if (thetas2.first.isNaN()) {
                    Float.NEGATIVE_INFINITY
                } else {
                    labelDiameter / 2f * max(cos(thetas2.first), cos(thetas2.second))
                }

                if (xMax1.isInfinite() && xMax2.isInfinite()) {
                    xLimit
                } else {
                    max(max(xMax1, xMax2), xLimit)
                }
            }

        var offset = Offset(xOffset, labelTop)

        // left side labels need x-position shifted left by their width
        if (isLeftSide) {
            offset += Offset(-placeable.width.toFloat(), 0f)
        }

        val anchorPoint = offset + Offset(0f, placeable.height / 2f) + if (isLeftSide) {
            Offset(placeable.width.toFloat(), 0f)
        } else {
            Offset(0f, 0f)
        }

        return ExternalLabelPosition(offset, anchorPoint, if (isLeftSide) 0.deg else 180.deg)
    }

    /**
     * Computes the position and anchor (line connection) position for a label when internal label placement
     * is used.
     */
    private fun computeInternalLabelOffset(
        pieParameters: PieParameters,
        sliceLabelData: SliceLabelData,
    ): LabelPosition {
        val pieSliceData = sliceLabelData.pieSliceData
        val placeable = sliceLabelData.labelPlaceable

        val radius = when (pieParameters.labelPlacement) {
            is PieLabelPlacement.Internal -> pieParameters.labelPlacement.radius
            is PieLabelPlacement.InternalOrExternal -> pieParameters.labelPlacement.radius
            else -> return None
        }

        val innerRadius = pieParameters.pieDiameter / 2f * pieParameters.holeSize
        val outerRadius = pieParameters.pieDiameter / 2f

        val labelCenter = polarToCartesian(
            outerRadius * radius,
            pieSliceData.startAngle + pieSliceData.angle / 2f
        )

        // test if the 4 corners of the placeable are contained within the slice area
        val c1 = cartesianToPolar(Offset(labelCenter.x - placeable.width / 2, labelCenter.y - placeable.height / 2))
        val c2 = cartesianToPolar(Offset(labelCenter.x + placeable.width / 2, labelCenter.y - placeable.height / 2))
        val c3 = cartesianToPolar(Offset(labelCenter.x - placeable.width / 2, labelCenter.y + placeable.height / 2))
        val c4 = cartesianToPolar(Offset(labelCenter.x + placeable.width / 2, labelCenter.y + placeable.height / 2))
        val bounded = isBounded(innerRadius, outerRadius, pieSliceData, c1) &&
                isBounded(innerRadius, outerRadius, pieSliceData, c2) &&
                isBounded(innerRadius, outerRadius, pieSliceData, c3) &&
                isBounded(innerRadius, outerRadius, pieSliceData, c4)

        return if (bounded) {
            InternalLabelPosition(Offset(labelCenter.x - placeable.width / 2, labelCenter.y - placeable.height / 2))
        } else {
            None
        }
    }

    /**
     * Tests if the point [pt] is within the bounds of the slice.
     */
    @Suppress("MagicNumber")
    private fun isBounded(
        innerRadius: Float,
        outerRadius: Float,
        pieSliceData: PieSliceData,
        pt: Polar
    ): Boolean {
        // normalize start angle to between 0 and 360
        val startNorm = ((pieSliceData.startAngle.degrees % 360.0) + 360.0) % 360.0
        val endNorm = startNorm + pieSliceData.angle.degrees // end angle > 0

        var pointAngle = pt.angle.degrees
        while (pointAngle < startNorm) {
            pointAngle += 360.0
        }

        return pointAngle < endNorm && pt.radius < outerRadius && pt.radius > innerRadius
    }
}

private class YState(var last: Float, var max: Float)

private data class PieParameters(
    val pieDiameter: Float,
    val holeSize: Float,
    val labelPlacement: PieLabelPlacement
)
